Java 的 new IO

Java 的 NIO 简介

about_Java 的 NIO 是于 1.4 版本引入的 IO 库,被称为 Java New IO 类库,简称 Java NIO。而在 Java NIO 引入之前的 Java IO 类库是阻塞 IO 也被称为 OIO(Older IO)。

Java NIO 的三个核心组件

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

是不是很眼熟呢?是的 Java NIO 属于 Input-Output 多路复用 模型。由 NIO 组件提供了统一的 API,使得程序员能在应用层控制和实现多路复用 IO

NIO 可以随意地读取 Buffer 中任意的数据并且组装成一个完整的数据,而 OIO 则只能顺序读取一个流中的数据。

NIO 的 Channel

一个 Channel (通道) 便对应着一个网络连接或者说一个文件描述符,和 OIO 中一个网络连接需要关联两个流 (InputStream,OutputStrem) 通过这两个流进行不断的输入和输出不同,Channel 既可以读也可以写,相当于两个流的结合。

NIO 的 Buffer

Buffer 是应用与 Channel 的沟通桥梁。NIO 使用 Buffer 缓冲区进行数据交互,- hannel 的读取就是将 Channel 的数据读到 Buffer 中,而 Channel 的写入则是将数据从 Buffer 中读到 Channel 中。
Netty、Redis、Zookeeper高并发实战#^f99d9a

NIO 的 Buffer 缓冲区本质上是一个内存块,它既可以写入数据也可以读取数据。

NIO 的 Selector

基于操作系统的 select 等实现, 它是一个 IO 时间的查询器。通过 Selector 可以在应用层面完成对多个文件描述符进行监视。而且一个 Selector 只需要一个 线程 监视,一个 Selector 可以通过管理多个 Channel 来管理多个文件描述符的状态,这样系统就不需要为每一个网络连接(文件描述符)创建 线程,极大的减少了系统的开销

Netty、Redis、Zookeeper高并发实战#3.5 详解NIO Selector选择器

Java NIO Buffer 类及其属性

Allocate()创建缓冲区

在使用 Buffer(缓冲区)之前,我们首先需要获取 Buffer 子类的实例对象,并且分配内存空间。

获取一个 Buffer 实例对象不是使用 new,而是调用 Buffer 子类的 allocate 方法:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
static IntBuffer intBuffer=null;

public void allocateTest(){  
    intBuffer = IntBuffer.allocate(20);  
    logger.info("---------intBuffer已创建---------");  
    logger.info("capacity="+intBuffer.capacity());  
    logger.info("position="+intBuffer.position());  
    logger.info("limit="+intBuffer.limit());  
}

在这个例子中调用了 InterBuffer.allocate(20) 创建了一个 IntBuffer 的实例对象,并且分配了 20 个 int 对象的空间也就是 20*4 个字节的空间。

例子的运行结果:

三月 25, 2023 2:01:03 下午 com.hingyun.bufferdemo.UseBuffer allocateTest
信息: ---------intBuffer已创建---------
三月 25, 2023 2:01:03 下午 com.hingyun.bufferdemo.UseBuffer allocateTest
信息: capacity=20
三月 25, 2023 2:01:03 下午 com.hingyun.bufferdemo.UseBuffer allocateTest
信息: position=0
三月 25, 2023 2:01:03 下午 com.hingyun.bufferdemo.UseBuffer allocateTest
信息: limit=20

从例子的运行结果中不难看出一个缓冲区新建后处于写入模式,其中 position 的写入位置为 0,最大的 limit 上线为容量 capacity 的初始化值。

Put 写入缓冲区

在调用 allocate 方法分配内存并返回实例对象后,缓冲区默认处于写模式,可以写入对象。写入缓冲区用到的是 put 方法,put 方法只需要一个参数就是需要写入的对象,但是这个参数的类型必须和缓冲区的类型保持一致。

现在我们调用之前的例子创建的 Buffer 对象,向里面写入 5 个 Int 类型的对象,也就是五个整数:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void putTest() {  
    allocateTest();  
    for (int i = 0; i < 5; i++) {  
        //写入一个整数到Buffer中  
        intBuffer.put(i);  
    }  
    logger.info("-------after put-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
}

在例子中我们向 Buffer 对象中写入了五个整数元素,下面是日志输出结果:

三月 25, 2023 2:40:50 下午 com.hingyun.bufferdemo.UseBuffer putTest
信息: -------after put-------
三月 25, 2023 2:40:50 下午 com.hingyun.bufferdemo.UseBuffer putTest
信息: capacity=20
三月 25, 2023 2:40:50 下午 com.hingyun.bufferdemo.UseBuffer putTest
信息: position=5
三月 25, 2023 2:40:50 下午 com.hingyun.bufferdemo.UseBuffer putTest
信息: limit=20

其中 capacity 和 limit 的值都和初始化的值一样没有发生变化,而 position 则变成了 5 指向了第 6 个位置,也就是代表我们刚刚写入了五个元素到缓冲区中接下来一个元素的写入会在第六个位置。

Flip () 翻转

在往缓冲区写入数据后是不能直接从缓冲区中读取数据的,此时缓冲区还处于写模式下,如果需要读取数据则还需要将缓冲区转换为度模式,这个时候就需要用到我们的 flip()翻转方法了。

紧接着前面的例子这是个 flip () 的方法的演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void flipTest(){  
    putTest();  
    intBuffer.flip();  
    logger.info("-------after flip-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
}

在调用 flip () 方法后缓冲区的属性就发生了变化:

信息: -------after flip-------
三月 25, 2023 2:52:23 下午 com.hingyun.bufferdemo.UseBuffer flipTest
信息: capacity=20
三月 25, 2023 2:52:23 下午 com.hingyun.bufferdemo.UseBuffer flipTest
信息: position=0
三月 25, 2023 2:52:23 下午 com.hingyun.bufferdemo.UseBuffer flipTest
信息: limit=5

从输出日志可以看出,在缓冲区翻转后缓冲区域的 capacity(容量)并没有发生变化,但是 position 的指向却到了 0 表示从头开始读取,而且 limit 的值也变成了之前 position 的值表示缓冲区中的最大可读数据量为 5。

由此我们可以得出缓冲区在写模式翻转成读模式时会先把 position 的值作为最大可读上限 limit 的值,然后设置 position 的值为 0 表示从头开始读。最后因为在例子中没有展示出来我提一嘴,在写模式翻转为读模式时会清除之前的 mark 标记,因为 mark 是写模式下的临时位置,如果在读模式下使用旧的 mark 标记会造成位置的混乱。

Flip () 方法的源码:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public final Buffer flip() {  
	//把 position 的值作为最大可读上限 limit 的值
    limit = position;  
    //设置 position 的值为 0 表示从头开始读
    position = 0;  
    //清除mark标记
    mark = -1;  
    return this;  
}

Get () 从缓冲区读取

在调用 flip 方法将缓冲区翻转后,紧接着我们来看一下如何从缓冲区读取数据,读取数据非常的简单,只需要调用 get()方法每次从 position 的位置读取一个数据,同时缓冲区的属性也会自动进行相应的调整。

演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void getTest() {  
    flipTest();  
    //读两个  
    for (int i = 0; i < 2; i++) {  
        logger.info("i=" + intBuffer.get());  
    }  
    logger.info("-------after get 1-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
    //再读三个  
    for (int i = 0; i < 3; i++) {  
        logger.info("i=" + intBuffer.get());  
    }  
    logger.info("-------after get 2-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
  
}

下面是输出日志:

三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------从缓冲区读两个元素-------
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=0
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=1
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------after get 1-------
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: capacity=20
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: position=2
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: limit=5
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------从缓冲区读三个元素-------
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=2
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=3
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=4
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------after get 2-------
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: capacity=20
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: position=5
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: limit=5

从输出日志我们可以看到读取操作会改变 position 的指向位置,而 limit 的值不会变,如果 position 和 limit 的值相等,则表示所有数据读取完毕,position 已经指向了一个没有数据的位置,已经无法读出任何数据了,如果此时再读就会抛出 BufferUnderflowException 异常。

那在我们在读完之后是否可以立刻进入写入模式对缓冲区进行写入呢?
这是不可以的,现在还是处于读模式,我们需要调用 Buffer.clearBuffer.compact 对缓冲区进行清空或者压缩,才能变为写入模式,这两个方法我们放在后面说。

既然读完后不能立刻进入写模式,那缓冲区可不可以重复读呢?

可以

Rewind()倒带

对于已经读完的数据,如果我们需要再读一遍的话可以使用 rewind()方法
演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void rewindTest() {  
    getTest();  
    intBuffer.rewind();  
    logger.info("-------after rewind-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
}

输出日志:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
信息: -------after rewind-------
三月 25, 2023 3:43:08 下午 com.hingyun.bufferdemo.UseBuffer rewindTest
信息: capacity=20
三月 25, 2023 3:43:08 下午 com.hingyun.bufferdemo.UseBuffer rewindTest
信息: position=0
三月 25, 2023 3:43:08 下午 com.hingyun.bufferdemo.UseBuffer rewindTest
信息: limit=5

从日志中可以看出 rewind 主要是调整了 position 属性,让 position 指向第一个元素并清空 mark,其他的值不会发生改变
Rewin 源码:

public final Buffer rewind() {  
    position = 0;  
    mark = -1;  
    return this;  
}

Mark 和 Reset

Buffer.mark() 方法的作用就是将 position 的值保存起来放在 mark 属性中,让 mark 属性记住这个临时的位置,而 reset 方法则是把 mark 属性的值恢复到 position 中。

演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void markTest() {  
    flipTest();  
    for (int i = 0; i < 5; i++) {  
        int j = intBuffer.get();  
        //当到第三个元素时mark这个位置  
        //注意这里是在get操作后进行的mark操作,
        //所以获取的是get操作后的position值,也就是2+1=3
        if (i == 2) {  
            intBuffer.mark();  
            logger.info("---mark---");  
            logger.info("position=" + intBuffer.position());  
        }  
    }  
    logger.info("-----before reset------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
  
  
    intBuffer.reset();  
    logger.info("-----after reset------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
}

日志输出:

信息: ---mark---
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: position=3
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: -----before reset------
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: capacity=20
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: position=5
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: limit=5
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: -----after reset------
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: capacity=20
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: position=3
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: limit=5

从日志中可以看到我们 mark 的位置是 3,在循环结束时 position 的指向为 5,而在我们调用 reset 方法后 position 又指向了 3,表示可以再次开始从第四个元素读取数据。

Clear 清空缓存区

在读取模式下,我们可以调用 clear() 方法将缓存区清空并切换为写入模式。这个方法会将 position 清零,limit 设置为 capacity 的最大容量值,可以一直写入,直到缓冲区写满。
演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void clearTest(){  
    //调用之前的读取方法  
    getTest();  
    //将缓存区清空  
    intBuffer.clear();  
    logger.info("-------after clear-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
  
    //写入buffer  
    for (int i = 0; i < 5; i++) {  
        intBuffer.put(i);  
    }  
    logger.info("-------after put-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
  
}

日志输出:

三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=0
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=1
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------after get 1-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: capacity=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: position=2
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: limit=5
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------从缓冲区读三个元素-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=2
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=3
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=4
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------after get 2-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: capacity=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: position=5
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: limit=5
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: -------after clear-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: capacity=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: position=0
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: limit=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: -------after put-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: capacity=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: position=5
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: limit=20

日志有一点长,不太适合阅读。总的来说 clear 方法的调用会让 position 值归零,limit = capacity,mark 值清空。
Clear 源码:

public final Buffer clear() {  
    position = 0;  
    limit = capacity;  
    mark = -1;  
    return this;  
}

小结

使用 Java NIO buffer 的基本步骤:

  1. 使用 allocate 创建子类实例
  2. 调用 put 向缓冲区写入数据
  3. 写入完成后,在读取之前调用 flip 方法将缓冲区转换为读取模式
  4. 调用 get 方法从缓冲区读取数据
  5. 读取完成后使用 clear 或者 compact 方法将缓冲区转换成写入模式


重要属性

Java NIO 的 Buffer 类是一个抽象类,它的内部是一个内存块(数组),与普通的 Java 数组不同的是:NIO Buffer 对象提供了一组更加有效的方法进行写入和读取的交替访问。为了记录读写的状态和位置 Buffer 类提供了一些重要的属性,其中有三个重要的成员属性 :capacity(容量)、position(读写位置)、limit(读写的限制)。另外还有一个 Mark 属性介意将当前的 position 记录下来,当需要时可以从 mark 标记中将 position 恢复到记录的位置。

注意:Buffer 类是一个非线程安全类

Buffer 类

Buffer 类是一个抽象类,对应于 Java 的主要数据类型,在 NIO 中有 8 种缓冲区类,分别如下:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer。

前七种类型包含了 about_Java 所有能在 IO 中传输的基本数据类型,Boolean 类型无法传输。第八种 MappedByteBuffer 是专门用于内存映射的 ByteBuffer 类型。

Buffer 类的重要属性

Capacity

Capacity(容量)限制着当前的 Buffer 缓冲区能存入的数据量。比如在 Buffer 类初始化对象时设置 capacity=20,则这个 Buffer 对象只能存入最多 20 个数据并且 capacity 属性一但初始化就不能更改。因为 Buffer 类在初始化时会按照 capacity 分配内部的内存,在内存分配好之后自然是不能改变了,而且 capacity 容量不是指内存 byte 的数量,而是指的写入的对象的数量(如一个 IntBuffer 初始化时 capacity 是 20 则这个 Buffer 最多只能存入 20 个 int 对象)

Position

Buffer 类中的 position 属性表示当前读或者写的位置。在读写模式下,position 属性是不同的,当缓冲区的读写模式转变后,position 会进行调整。Buffer 使用 flip 方法进行读写模式的切换,当 Buffer 进行 flip 翻转后,position 会由原来的写入位置变成新的可读位置,也就是 0,表示从头开始读。

在写入模式下 Position 的变化规则:
  1. 在刚进入写模式下 position 值为 0,表示当前写入位置从头开始。
  2. 每有一个数据写入 Buffer 中 position 就会向后移动一个可写位置,也就是 +1
  3. 当 position=limit 时,缓冲区就已经无空间可写。
在读模式下 Position 的变化规则:
  1. 当缓冲区进入读模式时,position 会重置为 0,意味着从头开始读取
  2. 当从缓冲区读取时,也是从 position 的位置开始读,在读取数据后 position 会移动到下一个可读的位置。
  3. Position 最大的值为最大可读上限 limit,当 position=limit 时代表缓冲区已经没有数据可读了。

Limiti

Buffer 类中的 limit 属性表示读写的最大上线,在读、写模式下 limit 表示的含义是不同的。在写模式下 limit 表示这个 Buffer 可以写入数据的最大上限,在 Buffer 初始化时 limit 默认等于 capacity;在读模式下 limit 的含义为能从 Buffer 中读到多少数据量。

一般来说 Buffer 的使用都是先写入再读取,毕竟去读一个空的 Buffer 是没有意义的。当缓冲区写入完成后可以使用 flip 翻转方法将模式改为写模式,这时 limit 的值会设置成写模式下的 position 值作为可读取得最大上限。

Mark

这时一个比较简单的属性,它的作用主要是记录当前 position 的值,以备需要时调用 reset 方法将 mark 值恢复到 position 中。

Java NIO Buffer 类的重要方法

Allocate()创建缓冲区

在使用 Buffer(缓冲区)之前,我们首先需要获取 Buffer 子类的实例对象,并且分配内存空间。

获取一个 Buffer 实例对象不是使用 new,而是调用 Buffer 子类的 allocate 方法:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
static IntBuffer intBuffer=null;

public void allocateTest(){  
    intBuffer = IntBuffer.allocate(20);  
    logger.info("---------intBuffer已创建---------");  
    logger.info("capacity="+intBuffer.capacity());  
    logger.info("position="+intBuffer.position());  
    logger.info("limit="+intBuffer.limit());  
}

在这个例子中调用了 InterBuffer.allocate(20) 创建了一个 IntBuffer 的实例对象,并且分配了 20 个 int 对象的空间也就是 20*4 个字节的空间。

例子的运行结果:

三月 25, 2023 2:01:03 下午 com.hingyun.bufferdemo.UseBuffer allocateTest
信息: ---------intBuffer已创建---------
三月 25, 2023 2:01:03 下午 com.hingyun.bufferdemo.UseBuffer allocateTest
信息: capacity=20
三月 25, 2023 2:01:03 下午 com.hingyun.bufferdemo.UseBuffer allocateTest
信息: position=0
三月 25, 2023 2:01:03 下午 com.hingyun.bufferdemo.UseBuffer allocateTest
信息: limit=20

从例子的运行结果中不难看出一个缓冲区新建后处于写入模式,其中 position 的写入位置为 0,最大的 limit 上线为容量 capacity 的初始化值。

Put 写入缓冲区

在调用 allocate 方法分配内存并返回实例对象后,缓冲区默认处于写模式,可以写入对象。写入缓冲区用到的是 put 方法,put 方法只需要一个参数就是需要写入的对象,但是这个参数的类型必须和缓冲区的类型保持一致。

现在我们调用之前的例子创建的 Buffer 对象,向里面写入 5 个 Int 类型的对象,也就是五个整数:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void putTest() {  
    allocateTest();  
    for (int i = 0; i < 5; i++) {  
        //写入一个整数到Buffer中  
        intBuffer.put(i);  
    }  
    logger.info("-------after put-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
}

在例子中我们向 Buffer 对象中写入了五个整数元素,下面是日志输出结果:

三月 25, 2023 2:40:50 下午 com.hingyun.bufferdemo.UseBuffer putTest
信息: -------after put-------
三月 25, 2023 2:40:50 下午 com.hingyun.bufferdemo.UseBuffer putTest
信息: capacity=20
三月 25, 2023 2:40:50 下午 com.hingyun.bufferdemo.UseBuffer putTest
信息: position=5
三月 25, 2023 2:40:50 下午 com.hingyun.bufferdemo.UseBuffer putTest
信息: limit=20

其中 capacity 和 limit 的值都和初始化的值一样没有发生变化,而 position 则变成了 5 指向了第 6 个位置,也就是代表我们刚刚写入了五个元素到缓冲区中接下来一个元素的写入会在第六个位置。

Flip () 翻转

在往缓冲区写入数据后是不能直接从缓冲区中读取数据的,此时缓冲区还处于写模式下,如果需要读取数据则还需要将缓冲区转换为度模式,这个时候就需要用到我们的 flip()翻转方法了。

紧接着前面的例子这是个 flip () 的方法的演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void flipTest(){  
    putTest();  
    intBuffer.flip();  
    logger.info("-------after flip-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
}

在调用 flip () 方法后缓冲区的属性就发生了变化:

信息: -------after flip-------
三月 25, 2023 2:52:23 下午 com.hingyun.bufferdemo.UseBuffer flipTest
信息: capacity=20
三月 25, 2023 2:52:23 下午 com.hingyun.bufferdemo.UseBuffer flipTest
信息: position=0
三月 25, 2023 2:52:23 下午 com.hingyun.bufferdemo.UseBuffer flipTest
信息: limit=5

从输出日志可以看出,在缓冲区翻转后缓冲区域的 capacity(容量)并没有发生变化,但是 position 的指向却到了 0 表示从头开始读取,而且 limit 的值也变成了之前 position 的值表示缓冲区中的最大可读数据量为 5。

由此我们可以得出缓冲区在写模式翻转成读模式时会先把 position 的值作为最大可读上限 limit 的值,然后设置 position 的值为 0 表示从头开始读。最后因为在例子中没有展示出来我提一嘴,在写模式翻转为读模式时会清除之前的 mark 标记,因为 mark 是写模式下的临时位置,如果在读模式下使用旧的 mark 标记会造成位置的混乱。

Flip () 方法的源码:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public final Buffer flip() {  
	//把 position 的值作为最大可读上限 limit 的值
    limit = position;  
    //设置 position 的值为 0 表示从头开始读
    position = 0;  
    //清除mark标记
    mark = -1;  
    return this;  
}

Get () 从缓冲区读取

在调用 flip 方法将缓冲区翻转后,紧接着我们来看一下如何从缓冲区读取数据,读取数据非常的简单,只需要调用 get()方法每次从 position 的位置读取一个数据,同时缓冲区的属性也会自动进行相应的调整。

演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void getTest() {  
    flipTest();  
    //读两个  
    for (int i = 0; i < 2; i++) {  
        logger.info("i=" + intBuffer.get());  
    }  
    logger.info("-------after get 1-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
    //再读三个  
    for (int i = 0; i < 3; i++) {  
        logger.info("i=" + intBuffer.get());  
    }  
    logger.info("-------after get 2-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
  
}

下面是输出日志:

三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------从缓冲区读两个元素-------
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=0
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=1
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------after get 1-------
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: capacity=20
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: position=2
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: limit=5
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------从缓冲区读三个元素-------
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=2
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=3
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=4
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------after get 2-------
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: capacity=20
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: position=5
三月 25, 2023 3:20:35 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: limit=5

从输出日志我们可以看到读取操作会改变 position 的指向位置,而 limit 的值不会变,如果 position 和 limit 的值相等,则表示所有数据读取完毕,position 已经指向了一个没有数据的位置,已经无法读出任何数据了,如果此时再读就会抛出 BufferUnderflowException 异常。

那在我们在读完之后是否可以立刻进入写入模式对缓冲区进行写入呢?
这是不可以的,现在还是处于读模式,我们需要调用 Buffer.clearBuffer.compact 对缓冲区进行清空或者压缩,才能变为写入模式,这两个方法我们放在后面说。

既然读完后不能立刻进入写模式,那缓冲区可不可以重复读呢?

可以

Rewind()倒带

对于已经读完的数据,如果我们需要再读一遍的话可以使用 rewind()方法
演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void rewindTest() {  
    getTest();  
    intBuffer.rewind();  
    logger.info("-------after rewind-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
}

输出日志:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
信息: -------after rewind-------
三月 25, 2023 3:43:08 下午 com.hingyun.bufferdemo.UseBuffer rewindTest
信息: capacity=20
三月 25, 2023 3:43:08 下午 com.hingyun.bufferdemo.UseBuffer rewindTest
信息: position=0
三月 25, 2023 3:43:08 下午 com.hingyun.bufferdemo.UseBuffer rewindTest
信息: limit=5

从日志中可以看出 rewind 主要是调整了 position 属性,让 position 指向第一个元素并清空 mark,其他的值不会发生改变
Rewin 源码:

public final Buffer rewind() {  
    position = 0;  
    mark = -1;  
    return this;  
}

Mark 和 Reset

Buffer.mark() 方法的作用就是将 position 的值保存起来放在 mark 属性中,让 mark 属性记住这个临时的位置,而 reset 方法则是把 mark 属性的值恢复到 position 中。

演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void markTest() {  
    flipTest();  
    for (int i = 0; i < 5; i++) {  
        int j = intBuffer.get();  
        //当到第三个元素时mark这个位置  
        //注意这里是在get操作后进行的mark操作,
        //所以获取的是get操作后的position值,也就是2+1=3
        if (i == 2) {  
            intBuffer.mark();  
            logger.info("---mark---");  
            logger.info("position=" + intBuffer.position());  
        }  
    }  
    logger.info("-----before reset------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
  
  
    intBuffer.reset();  
    logger.info("-----after reset------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
}

日志输出:

信息: ---mark---
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: position=3
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: -----before reset------
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: capacity=20
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: position=5
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: limit=5
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: -----after reset------
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: capacity=20
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: position=3
三月 25, 2023 4:33:25 下午 com.hingyun.bufferdemo.UseBuffer markTest
信息: limit=5

从日志中可以看到我们 mark 的位置是 3,在循环结束时 position 的指向为 5,而在我们调用 reset 方法后 position 又指向了 3,表示可以再次开始从第四个元素读取数据。

Clear 清空缓存区

在读取模式下,我们可以调用 clear() 方法将缓存区清空并切换为写入模式。这个方法会将 position 清零,limit 设置为 capacity 的最大容量值,可以一直写入,直到缓冲区写满。
演示:

/**  
 * @author: Ten  
 * @date: 2023/3/25 13:44  
 */
public void clearTest(){  
    //调用之前的读取方法  
    getTest();  
    //将缓存区清空  
    intBuffer.clear();  
    logger.info("-------after clear-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
  
    //写入buffer  
    for (int i = 0; i < 5; i++) {  
        intBuffer.put(i);  
    }  
    logger.info("-------after put-------");  
    logger.info("capacity=" + intBuffer.capacity());  
    logger.info("position=" + intBuffer.position());  
    logger.info("limit=" + intBuffer.limit());  
  
}

日志输出:

三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=0
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=1
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------after get 1-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: capacity=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: position=2
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: limit=5
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------从缓冲区读三个元素-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=2
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=3
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: i=4
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: -------after get 2-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: capacity=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: position=5
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer getTest
信息: limit=5
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: -------after clear-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: capacity=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: position=0
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: limit=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: -------after put-------
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: capacity=20
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: position=5
三月 25, 2023 4:53:28 下午 com.hingyun.bufferdemo.UseBuffer clearTest
信息: limit=20

日志有一点长,不太适合阅读。总的来说 clear 方法的调用会让 position 值归零,limit = capacity,mark 值清空。
Clear 源码:

public final Buffer clear() {  
    position = 0;  
    limit = capacity;  
    mark = -1;  
    return this;  
}

小结

使用 Java NIO buffer 的基本步骤:

  1. 使用 allocate 创建子类实例
  2. 调用 put 向缓冲区写入数据
  3. 写入完成后,在读取之前调用 flip 方法将缓冲区转换为读取模式
  4. 调用 get 方法从缓冲区读取数据
  5. 读取完成后使用 clear 或者 compact 方法将缓冲区转换成写入模式


重要属性

Java NIO 的 Buffer 类是一个抽象类,它的内部是一个内存块(数组),与普通的 Java 数组不同的是:NIO Buffer 对象提供了一组更加有效的方法进行写入和读取的交替访问。为了记录读写的状态和位置 Buffer 类提供了一些重要的属性,其中有三个重要的成员属性 :capacity(容量)、position(读写位置)、limit(读写的限制)。另外还有一个 Mark 属性介意将当前的 position 记录下来,当需要时可以从 mark 标记中将 position 恢复到记录的位置。

注意:Buffer 类是一个非线程安全类

Buffer 类

Buffer 类是一个抽象类,对应于 Java 的主要数据类型,在 NIO 中有 8 种缓冲区类,分别如下:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer。

前七种类型包含了 about_Java 所有能在 IO 中传输的基本数据类型,Boolean 类型无法传输。第八种 MappedByteBuffer 是专门用于内存映射的 ByteBuffer 类型。

Buffer 类的重要属性

Capacity

Capacity(容量)限制着当前的 Buffer 缓冲区能存入的数据量。比如在 Buffer 类初始化对象时设置 capacity=20,则这个 Buffer 对象只能存入最多 20 个数据并且 capacity 属性一但初始化就不能更改。因为 Buffer 类在初始化时会按照 capacity 分配内部的内存,在内存分配好之后自然是不能改变了,而且 capacity 容量不是指内存 byte 的数量,而是指的写入的对象的数量(如一个 IntBuffer 初始化时 capacity 是 20 则这个 Buffer 最多只能存入 20 个 int 对象)

Position

Buffer 类中的 position 属性表示当前读或者写的位置。在读写模式下,position 属性是不同的,当缓冲区的读写模式转变后,position 会进行调整。Buffer 使用 flip 方法进行读写模式的切换,当 Buffer 进行 flip 翻转后,position 会由原来的写入位置变成新的可读位置,也就是 0,表示从头开始读。

在写入模式下 Position 的变化规则:
  1. 在刚进入写模式下 position 值为 0,表示当前写入位置从头开始。
  2. 每有一个数据写入 Buffer 中 position 就会向后移动一个可写位置,也就是 +1
  3. 当 position=limit 时,缓冲区就已经无空间可写。
在读模式下 Position 的变化规则:
  1. 当缓冲区进入读模式时,position 会重置为 0,意味着从头开始读取
  2. 当从缓冲区读取时,也是从 position 的位置开始读,在读取数据后 position 会移动到下一个可读的位置。
  3. Position 最大的值为最大可读上限 limit,当 position=limit 时代表缓冲区已经没有数据可读了。

Limiti

Buffer 类中的 limit 属性表示读写的最大上线,在读、写模式下 limit 表示的含义是不同的。在写模式下 limit 表示这个 Buffer 可以写入数据的最大上限,在 Buffer 初始化时 limit 默认等于 capacity;在读模式下 limit 的含义为能从 Buffer 中读到多少数据量。

一般来说 Buffer 的使用都是先写入再读取,毕竟去读一个空的 Buffer 是没有意义的。当缓冲区写入完成后可以使用 flip 翻转方法将模式改为写模式,这时 limit 的值会设置成写模式下的 position 值作为可读取得最大上限。

Mark

这时一个比较简单的属性,它的作用主要是记录当前 position 的值,以备需要时调用 reset 方法将 mark 值恢复到 position 中。

Java NIO Channel 类

Info

之前提到过在 NIO 中一个连接就是使用一个 channel 表示,也就是一个通道可以表示一个底层的文件描述符,比如硬件设备、文件、网络连接等。其实不止如此,除了了对应底层的文件描述符外,Java NIO 的通道还可以细化到不同的网络传输协议,对于不同的协议 Java NIO 都有不同的实现。

Channel 的主要类型

Channel 使用比较多的类型主要有四种

名称 说明 备注
FileChannel 文件通道,主要用于文件读写 只有阻塞模式,因为对于文件的读写不需要去轮询内核数据是否准备好
SocketChannel 用于 socket 套接字 TCP 链接的数据读写 在客户端和服务端都存在
ServerSocketChannel 服务器套接字通道,允许程序监听 TCP 连接请求,为每个监听到的请求创建一个 SocketChannel 通道 只存在于服务端
DatagramChannel 用于 UDP 协议的数据读写

FileChannel 文件通道

获取 filechannel 通道

可以通过文件的输入流、输出流获取FileChannel通道,又或者通过RandomAccessFile文件随机访问类获取通道

1680087161

从filechannel通道中读取数据

在大部分的应用场景中,从通道读取数据都会调用通道的int read 写入到Buffer 中,也就是之前说过的ByteBufferbuf方法,它将从通道的读取的数据写入到ByteBuffer缓冲区中

从FileChannel中读取数据代码示例

注意

通道读取数据对于Buffer缓冲区来说是写入数据,此时缓冲区处于写入模式

写入filechannel通道

同样的大部分应用场景在往通道写入数据时是int write (ByteBufferbuf) 方法,此时缓冲区就是写入数据的来源。通过调用write()方法从Buffer中读取数据,然后写入到通道中,返回值就是写入成功的字节数

写入FileChannel通道代码示例

注意

在向通道中写入数据时要求缓冲区是可读的,需要缓冲区翻转成读模式。

在使用完通道后一定要关闭通道,使用close即可

channel.close

强制刷新到磁盘

在将缓冲数据写入通道时,出于对性能考虑,操作系统不能每次都实时将数据写入磁盘。如果需要保证将缓冲数据立刻写入磁盘需要调用force()方法强制刷新到磁盘

channel.force(true)

SocketChannel 套接字通道

在 NIO 中 SocketChannel 与 ServerSocketChannel 是相辅相成的,一个负责连接传输,一个负责连接监听。他们两个都支持阻塞和非阻塞两种模式。调用 socketChannel.configureBlocking(Boolean) 方法即可修改模式,考虑到效率方面,阻塞模式基本不会使用到。

获取 SocketChannel 通道

客户端获取通道

  1. 通过 SocketChannel 的 open 方法获得一个传输通道
  2. 将 socket 设置为非阻塞模式
  3. 对服务器 IP 和端口发起连接

1680087190

服务器端获取套接字

通过调用服务器端 ServerSocketChannel 监听套接字的 accept() 方法,来获取新连接的套接字通道

20230328

Java NIO Selector 选择器

Note

选择器的使命就是完成 IO 多路复用,通过选择器可以同时监控多个通道的 IO 事件。选择器与通道的关系就是监控和被监控的关系

基于之前对 IO 多路复用 的理解我们可以知道,一条线程处理一个选择器,而一个选择器可以监控很多的通道。所以这意味着通过选择器一条线程可以处理成百上千的通道,大量的减少了线程之间上下文切换的开销。

通道和选择器之间通过 register (注册) 的方式完成,调用通道的 Channel.register(Selector sel, int ops) 方法,可以将通道实例注册到一个选择器中。第一个参数指定选择器实例,第二个参数指定 IO 事件类型。

  • 📌 可供选择器监控的通道IO事件类型,包括以下四种:(1)可读:SelectionKey.OP_READ(2)可写:SelectionKey.OP_WRITE(3)连接:SelectionKey.OP_CONNECT(4)接收:SelectionKey.OP_ACCEPT

事件类型的定义在 SelectionKey 类中。如果选择器要监控通道的多种时间,可以用 " 按位或 " 运算符来实现。

        //监控通道的多种事件,用“按位或”运算符来实现
        int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
注意

这里的 IO 事件指的不是对通道的 IO 操作,而是通道的某个 IO 操作的一种就绪状态,比如 SocketChannel 通道, 完成握手连接则处于 " 连接就绪状态 " (OP_CONNECT)

^93799f

判断一个通道能否被选择

一个通道是否能被选择,首先要看是否继承 SelectableChannel 类,如果继承了就可以被选择,否则不能。像 FileChannel 就没有继承 SelectableChannel,所以不可以选择的通道。

SelectionKey 选择键

一旦在通道中发生了某些 IO 事件(就绪状态达成),并且是在选择器中注册过的 IO 事件,就会被选择器选中,并放入 SelectionKey 选择键的集合中。

Note

SelectionKey 选择键就是那些被选择器选中的 IO 事件,那些没有被注册过的通道即使发生了 IO 事件也不会被选择器选中放入选择键集合中

通过选择键不仅仅可获得通道的 IO 事件类型,也可以获得 IO 事件所在的通道,还有选出选择键的选择器实例